package com.ls.fw.mvc.springmvc.support.arg;

import java.lang.annotation.Annotation;
import java.util.Map;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.BeanUtils;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.DataBinder;
import org.springframework.validation.Errors;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.ls.fw.mvc.springmvc.support.annotation.PathVariables;
import com.ls.fw.mvc.springmvc.support.arg.form.BaseMethodArgumentResolver;
import com.ls.fw.web.core.servlet.utils.HttpUtils;

public class PathVariablesArgumentResolver extends BaseMethodArgumentResolver {

	private AntPathMatcher matcher = new AntPathMatcher();

	public boolean supportsParameter(MethodParameter parameter) {
		return parameter.getParameterAnnotation(PathVariables.class) != null;
	}

	public Object resolveArgument(MethodParameter parameter,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
			WebDataBinderFactory binderFactory) throws Exception {
		PathVariables attr = parameter
				.getParameterAnnotation(PathVariables.class);

		String attrName = (attr != null) ? attr.value() : parameter
				.getParameterName();
		attrName = (org.apache.commons.lang3.StringUtils.isBlank(attrName)) ? parameter.getParameterName(): attrName;
		String name = StringUtils.hasText(attrName) ? attrName : Conventions
				.getVariableNameForParameter(parameter);
		// 获取绑定对象,如果model中存在直接获取否则新建一个
		Object target = (mavContainer.containsAttribute(name)) ? mavContainer
				.getModel().get(name) : createAttribute(name, parameter,
				binderFactory, webRequest);
		if(target instanceof String){
			target = HttpUtils.decode(String.valueOf(target));
		}
		WebDataBinder binder = binderFactory.createBinder(webRequest, target,
				name);
		target = binder.getTarget();
		if (target != null) {

			bindRequestParameters(binder, webRequest);

			validateIfApplicable(binder, parameter);
			if (binder.getBindingResult().hasErrors()) {
				if (isBindExceptionRequired(binder, parameter)) {
					throw new BindException(binder.getBindingResult());
				}
			}
		}

		target = binder.convertIfNecessary(binder.getTarget(),
				parameter.getParameterType());
		// 将绑定结果和认证结果添加到model
		Map<String, Object> bindingResultModel = binder.getBindingResult()
				.getModel();

		mavContainer.removeAttributes(bindingResultModel);
		mavContainer.addAllAttributes(bindingResultModel);
		return target;
	}

	// 绑定参数
	private void bindRequestParameters(WebDataBinder binder,
			NativeWebRequest request) {
		ServletRequest servletRequest = request
				.getNativeRequest(ServletRequest.class);
		ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
		servletBinder.bind(servletRequest);
	}

	/**
	 * Whether to raise a {@link org.springframework.validation.BindException}
	 * on bind or validation errors. The default implementation returns
	 * {@code true} if the next method argument is not of type
	 * {@link org.springframework.validation.Errors}.
	 *
	 * @param binder
	 *            the data binder used to perform data binding
	 * @param parameter
	 *            the method argument
	 */
	protected boolean isBindExceptionRequired(WebDataBinder binder,
			MethodParameter parameter) {
		int i = parameter.getParameterIndex();
		Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
		boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class
				.isAssignableFrom(paramTypes[i + 1]));

		return !hasBindingResult;
	}

	/**
	 * Validate the model attribute if applicable.
	 * <p>
	 * The default implementation checks for {@code @javax.validation.Valid}.
	 *
	 * @param binder
	 *            the DataBinder to be used
	 * @param parameter
	 *            the method parameter
	 */
	protected void validateIfApplicable(WebDataBinder binder,
			MethodParameter parameter) {
		Annotation[] annotations = parameter.getParameterAnnotations();
		for (Annotation annot : annotations) {
			if (annot.annotationType().getSimpleName().startsWith("Valid")) {
				Object hints = AnnotationUtils.getValue(annot);
				binder.validate(hints instanceof Object[] ? (Object[]) hints
						: new Object[] { hints });
			}
		}
	}

	private Object createAttribute(String attributeName,
			MethodParameter parameter, WebDataBinderFactory binderFactory,
			NativeWebRequest request) throws Exception {

		// 如果request中存在该attributeName,则尝试用ConversionService转换
		String value = getRequestValueForAttribute(parameter, attributeName,
				request);
		if (value != null) {
			Object attribute = createAttributeFromRequestValue(value,
					attributeName, parameter, binderFactory, request);
			if (attribute != null) {
				return attribute;
			}
		}

		// 否则直接创建
		return BeanUtils.instantiateClass(parameter.getParameterType());
	}

	// 从request中获取attributeName
	private String getRequestValueForAttribute(MethodParameter parameter,
			String attributeName, NativeWebRequest webRequest) {
		Map<String, String> variables = getUriTemplateVariables(webRequest);
		if (StringUtils.hasText(variables.get(attributeName))) {
			return variables.get(attributeName);
		} else if (StringUtils.hasText(webRequest.getParameter(attributeName))) {
			return webRequest.getParameter(attributeName);
		} else {
			String value = null;
			RequestMapping mapping = parameter.getMethod().getAnnotation(
					RequestMapping.class);
			HttpServletRequest request = null;
			if (webRequest.getNativeRequest() instanceof HttpServletRequest) {
				request = (HttpServletRequest) webRequest.getNativeRequest();
			}
			String[] patternString = mapping.value();
			String path = request.getRequestURI();
			path = path.replaceFirst(request.getContextPath(), "");
			mapping = parameter.getContainingClass().getAnnotation(
					RequestMapping.class);
			for (String string : patternString) {
				if (matcher.match(mapping.value()[0] + string, path)) {
					value = this.extractPathWithinPattern(mapping.value()[0]
							+ string, path, true);
					if (org.apache.commons.lang3.StringUtils.isNotBlank(value)) {
						break;
					}
				}
			}
			return value;
		}
	}

	// 从requestValue创建Attribute
	private Object createAttributeFromRequestValue(String sourceValue,
			String attributeName, MethodParameter parameter,
			WebDataBinderFactory binderFactory, NativeWebRequest request)
			throws Exception {
		DataBinder binder = binderFactory.createBinder(request, null,
				attributeName);
		ConversionService conversionService = binder.getConversionService();
		if (conversionService != null) {
			TypeDescriptor source = TypeDescriptor.valueOf(String.class);
			TypeDescriptor target = new TypeDescriptor(parameter);
			if (conversionService.canConvert(source, target)) {
				return binder.convertIfNecessary(sourceValue,
						parameter.getParameterType(), parameter);
			}
		}
		return null;
	}

	public String extractPathWithinPattern(String pattern, String path,
			boolean returnValid) {
		String[] patternParts = StringUtils.tokenizeToStringArray(pattern, "/",
				true, true);
		String[] pathParts = StringUtils.tokenizeToStringArray(path, "/", true,
				true);

		StringBuilder builder = new StringBuilder();

		// Add any path parts that have a wildcarded pattern part.
		int start = 0, end = 0;
		for (int i = 0; i < patternParts.length; i++) {
			String patternPart = patternParts[i];
			if (start == 0
					&& (patternPart.indexOf("**") > -1 || patternPart
							.indexOf('?') > -1) && pathParts.length >= i + 1) {
				start = i;
				continue;
			}
			if (start > 0 && patternPart.indexOf('*') <= -1) {
				end++;
			}
		}
		// Append any trailing path parts.
		int addSize = 0, size = pathParts.length - end;
		for (int i = start; i < size; i++) {
			if (!(returnValid && pathParts[i].startsWith("."))) {
				if (addSize > 0) {
					builder.append("/");
				}
				builder.append(pathParts[i]);
				addSize++;
			}
		}

		return builder.toString();
	}
}
